你是個寂寞的少女嗎?還是抑鬱的少男?Yo!正當世人追逐著那些躁動的演唱,我們宅著、坐著、躺著、開著 browser。常常點開 youtube 讓意識傾斜聽覺chill鬆嗎?「沒有更興奮的聲音嗎?」網頁一個自由的空間中,卻不自由的成為被餵養的獸。聽覺經驗狹隘的前震動時期,HTML 中唯讀 <audio>
在醜陋黑暗的原始碼 Ghetto 中默默接歌放歌。沒有創作的空間,沒有隨意可以調動的 fader。不夠!這不夠!
W3C 聽到了人心中隱隱震動的聲音,人們的聽覺神經已經從冬眠中甦醒,偷偷呼喚著更強大、更複雜、更多元的聲音介面。Web Audio API 相應而生:
…(Web Audio API) defines a client-side script API adding more advanced audio capabilities than are currently offered by audio elements. The API supports the features required by advanced interactive application including the ability to process and synthesize audio streams directly in script.
by Audio Working Group
喔!”Process”!”Synthesize”!多麽令人興奮的單字!跟隨科幻的腳步,流瀉了音符出自 client-side,奔放數學式組織著 sine 波、鋸齒波,上下跳動。彷彿買到了瀏覽器終於裝上了 CDPRO2。
黑盒子的裡面,Web Audio API 透過建立 AudioNode
這種形態的 objects,並互相連接而串接出整個 audio rendering map。先來嗑一張圖,就像我曾說過的喝符水。如下,圖中一顆一顆的就是 AudioNode,而所有的 nodes 都被包在 AudioContext 裡面,而 node 又分成 Source 跟 GainNode 兩種。容我娓娓道來。
一切 Web Audio API 的實作中,底層有許多複雜的 Assembly / C / C++ 的程式碼。API 直接簡化到操作與管理全部都在物件 AudioContext 內部,例如想要表格填完送出一秒之後產生一個爆炸的聲音嚇死你的客戶的話,可能就需要利用 AudioContext.currentTime 去取得點擊時間與進行聲音 event 排程。所有的 AudioNode 想當然爾也都在 AudioContext 裡面被出生、教育、工作、死亡。若是使用<canvas>
的經驗,可以想像就是跟canvas.getContext
類似的作用與邏輯。
let context = new (window.AudioContext || window.webkitAudioContext)();
常見的 Web Audio API workflow 可以大致上歸類如下:
AudioContext
SourceNode
SourceNode
連結到放大器(gain)、 濾波器(filter)等訊號處理的 AudioNodeAudioNode
接到 AudioContext
的 destination
以輸出聲音聲音總得有個震動的來源吧?樂團中,主唱唱歌時震動的來源是聲帶,吉他撕裂的聲響透過效果器送到喇吧,來源則是播弦產生的震盪;Web Audio 則需要創建 SourceNode
,其大致可以分為三種:
如何創建 SourceNode
呢?最簡單就是直接呼叫AudioContext.createOscillator
:
let context = new (window.AudioContext || window.webkitAudioContext)();
let oscillator = context.createOscillator();
oscillator.type = 'sine’;
oscillator.frequency.value = 440;
oscillator.connect(context.destination);
oscillator.start();
上面所設定的oscillator.type
指的是不同的波形(如下圖),可以有多種不同的設定,完整的運作起來就會像是這樣:
https://codepen.io/gregh/full/LxJEaj/
聽!是吉他聲從煙霧彌漫的 live house 裡面傳出來,懷念哪!那種頹廢的週六夜晚,流蕩在街頭的大鼓隱隱作響。真的,有些東西是單純的波形合成製造不出來的!我們需要更多的音源。
無法直接如同圖片檔案用 url 馬上抓,而需要三步驟:
XMLHttpRequest
抓取之後class Buffer {
constructor(context, urls) {
this.context = context;
this.urls = urls;
this.buffer = [];
}
loadSound(url, index) {
let request = new XMLHttpRequest();
request.open('get', url, true);
request.responseType = 'arraybuffer';
let thisBuffer = this;
request.onload = function() {
thisBuffer.context.decodeAudioData(request.response, function(buffer) {
thisBuffer.buffer[index] = buffer;
updateProgress(thisBuffer.urls.length);
if(index == thisBuffer.urls.length-1) {
thisBuffer.loaded();
}
});
};
request.send();
};
loadAll() {
this.urls.forEach((url, index) => {
this.loadSound(url, index);
})
}
loaded() {
// 檔案載入完成之後執行這個 function
}
getSoundByIndex(index) {
return this.buffer[index];
}
}
以這樣的邏輯進行下去,很快就可以做出像是下面這個美麗的吉他。
GainNode 非常好理解,就只是訊號放大或是縮小的節點。
const gain = context.createGain();
oscillator.connect(gain);
gain.connect(context.destination);
彈過吉他的傢伙就知道什麼叫做 filter,旋鈕催下去就彷彿進入另外一個故事。濾波器就像是守門員,幫你擋掉聲音不想要的頻段。舉兩個簡單的例子,High Pass 就是只讓某個頻率以上的聲音通過、Low Pass 就是只讓某個頻率以下的聲音通過,使用的方式也不難:
// example from https://developer.mozilla.org/
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
//set up the different audio nodes we will use for the app
var analyser = audioCtx.createAnalyser();
var distortion = audioCtx.createWaveShaper();
var gainNode = audioCtx.createGain();
var biquadFilter = audioCtx.createBiquadFilter();
var convolver = audioCtx.createConvolver();
// connect the nodes together
source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(biquadFilter);
biquadFilter.connect(convolver);
convolver.connect(gainNode);
gainNode.connect(audioCtx.destination);
// Manipulate the Biquad filter
biquadFilter.type = "lowshelf";
biquadFilter.frequency.value = 1000;
biquadFilter.gain.value = 25;
那樣的感覺又來了,開個聲音他媽怎麼那麼多步驟?framework 像是仲介公司一樣,幫你解決那些狗屁倒灶的鳥事。明天就來講 audio framework。打完收工。
關於作者
Vibert Thio
致力於將對於技術的深度研究轉化為新型態藝術創作的能量,並思考技術的拓展/侷限與其對於藝術論述/呈現的影響。專長為數位藝術創作、音像程式設計、互動設計,喜愛即時運算的臨場感與不可預測。